<template>
  <div
    class="el-tree"
    :class="{
      'el-tree--highlight-current': highlightCurrent,
      'is-dragging': !!dragState.draggingNode,
      'is-drop-not-allow': !dragState.allowDrop,
      'is-drop-inner': dragState.dropType === 'inner'
    }"
    :style="{
      maxHeight: maxHeight + 'px',
      overflow: 'auto'
    }"
    @scroll="handleScroll"
    role="tree"
  >
    <!-- <div style="position: fixed;">{{rangerInfo.min}}-{{rangerInfo.max}}</div> -->
    <Show-index-range
        :tree="tree"
        :style="{
          position: 'relative',
          height: itemHeight * (maxShowIndex - root.showIndex) + 'px'
        }"
    >
    <el-tree-node
      v-for="child in root.childNodes"
      :node="child"
      :props="props"
      :style="{
            position: 'absolute',
            width: '100%',
            top: (child.showIndex - root.showIndex - 1) * itemHeight + 'px'
          }"
      :show-index="child.showIndex"
      :need-filter="child.isLeaf"   
      :render-after-expand="renderAfterExpand"
      :key="getNodeKey(child)"
      :render-content="renderContent"
      @node-expand="handleNodeExpand">
    </el-tree-node>
    </Show-index-range>
    <div class="el-tree__empty-block" v-if="!root.childNodes || root.childNodes.length === 0">
      <span class="el-tree__empty-text">{{ emptyText }}</span>
    </div>
    <div
      v-show="dragState.showDropIndicator"
      class="el-tree__drop-indicator"
      ref="dropIndicator">
    </div>
  </div>
</template>

<script lang="ts">
import { Component, Prop, Vue, Mixins, Watch, Provide } from 'vue-property-decorator';
import TreeStore from './model/tree-store'
import { getNodeKey, findNearestComponent } from './model/util'
import ElTreeNode from './tree-node.vue'
import Emitter from '../polyfill/emitter'
import { addClass, removeClass } from '../polyfill/dom.js'
import ShowIndexRange from './ShowIndexRange.vue'
@Component({
  name: 'ElTree',
  components: { ElTreeNode, ShowIndexRange }
})

export default class ElTree extends Mixins(Emitter) {
  private store: any = null
  private root: any = null
  private currentNode: any = null
  private treeItems: any = null
  private checkboxItems: any = []
  private dragState = {
    showDropIndicator: false,
    draggingNode: null,
    dropNode: null,
    allowDrop: true
  }
  private scrollTop = 0
  private acc = 0
  private rangerInfo = {
    min: 0,
    max: 0
  }
  @Prop({default: 260})  private maxHeight: any
  @Prop({default: '260px'})  private height: any
  @Prop({default: 26})  private itemHeight: any
  @Prop({default: 260})  private data: any
  @Prop({default: true })  private renderAfterExpand: any
  @Prop({default: '空节点'})  private emptyText: any
  @Prop({default: ''})  private nodeKey: any
  @Prop({default: false })  private checkStrictly: boolean | undefined
  @Prop({default: false })  private defaultExpandAll: boolean | undefined
  @Prop({default: true }) private expandOnClickNode: boolean |undefined
  @Prop({}) private checkOnClickNode: boolean |undefined
  @Prop({default: false }) private checkDescendants: boolean |undefined
  @Prop({default: true }) private autoExpandParent: boolean |undefined
  @Prop() private defaultCheckedKeys: any
  @Prop() private defaultExpandedKeys: any
  @Prop() private renderContent: any
  @Prop({default: false }) private showCheckbox: boolean | undefined
  @Prop({default: false }) private draggable: boolean | undefined
  @Prop() private allowDrag: any
  @Prop() private allowDrop: any
  @Prop( { default: {
    children: 'children',
    maxShowIndex: 'label',
    icon: 'icon',
    disabled: 'disabled'
  }}) private props: any
  @Prop({default: false}) private lazy: boolean | undefined
  @Prop() private highlightCurrent: any
  @Prop() private load: any
  @Prop() private filterNodeMethod: any
  @Prop() private accordion: any
  @Prop({default: 18}) private indent: any
  @Provide()
  private ranger = this.rangerInfo
  @Provide('itemheight')
  private itemheight = (this as any).itemHeight
  get tree () {
    return this
  }
  get children () {
    return this.data
  }
  set children (value) {
    this.data = value
  }

  get maxShowIndex () {
    return (this as any).root.maxShowIndex  || 0
  }
  get treeItemArray () {
    return Array.prototype.slice.call(this.treeItems)
  }

  @Watch('scrollTop', { immediate: true})
  private scrollTopChange () {
    let min = ~~(this.scrollTop / (this as any).itemHeight)
    let max = min + ~~(this.maxHeight / (this as any).itemHeight)
    this.rangerInfo.min = Math.max(min - 10, 0)
    this.rangerInfo.max = max + 10
  }
  @Watch('defaultCheckedKeys')
  private defaultCheckedKeysChange (newVal) {
    (this as any).store.defaultCheckedKeys = newVal
    (this as any).store.setDefaultCheckedKey(newVal)
  }

  
  @Watch('data')
  private dataChange (newVal) {
    (this as any).store.setData(newVal)
  }

  @Watch('checkboxItems')
  private checkboxItemsChange (val) {
    Array.prototype.forEach.call(val, (checkbox) => {
      checkbox.setAttribute('tabindex', -1)
    })
  }

  @Watch('checkStrictly')
  private checkStrictlyChange (newVal) {
    (this as any).store.checkStrictly = newVal
  }

  private reset () {
    this.scrollTop = this.$el.scrollTop
  }
  
  private handleScroll () {
      const scrollTop = this.$el.scrollTop
      // var oldScrollTop = this.$_oldScrollTop || scrollTop

      // this.acc = scrollTop - oldScrollTop

      // if (this.acc > 1000) {
      //   this.acc = 1000
      //   scrollTop = this.$el.scrollTop = oldScrollTop + 1000
      // }

      // this.$_oldScrollTop = scrollTop
      this.scrollTop = scrollTop
    }
  
  private filter (value) {
    if (!this.filterNodeMethod) {
      throw new Error('[Tree] filterNodeMethod is required when filter')
    } 
    (this as any).store.filter(value)
  }

  private getNodeKey (node) {
    return getNodeKey(this.nodeKey, node.data)
  }

  private getNodePath (data) {
    if (!this.nodeKey) {
      throw new Error('[Tree] nodeKey is required in getNodePath')
    }
    const node = (this as any).store.getNode(data)
    if (!node) { 
      return []
    }
    const path = [node.data]
    let parent = node.parent
    while (parent && parent !== this.root) {
      path.push(parent.data)
      parent = parent.parent
    }
    return path.reverse()
  }

  private getCheckedNodes (leafOnly) {
    return (this as any).store.getCheckedNodes(leafOnly)
  }

  private getCheckedKeys (leafOnly) {
      return this.store.getCheckedKeys(leafOnly)
  }

  private getCurrentNode () {
    const currentNode = this.store.getCurrentNode()
    return currentNode ? currentNode.data : null
  }

  private getCurrentKey () {
    if (!this.nodeKey) {
      throw new Error('[Tree] nodeKey is required in getCurrentKey')
    }
    const currentNode = this.getCurrentNode()
    return currentNode ? currentNode[this.nodeKey] : null
  }

  private  setCheckedNodes (nodes, leafOnly) {
    if (!this.nodeKey) {
      throw new Error('[Tree] nodeKey is required in setCheckedNodes')
    }
    this.store.setCheckedNodes(nodes, leafOnly)
  }

  private setCheckedKeys (keys, leafOnly) {
    if (!this.nodeKey) {
      throw new Error('[Tree] nodeKey is required in setCheckedKeys')
    }
    this.store.setCheckedKeys(keys, leafOnly)
  }

  private setChecked (data, checked, deep) {
    this.store.setChecked(data, checked, deep)
  }

  private getHalfCheckedNodes () {
    return this.store.getHalfCheckedNodes()
  }

  private getHalfCheckedKeys () {
    return this.store.getHalfCheckedKeys()
  }

  private setCurrentNode (node) {
    if (!this.nodeKey) {
      throw new Error('[Tree] nodeKey is required in setCurrentNode')
    }
    this.store.setUserCurrentNode(node)
  }

  private setCurrentKey (key) {
    if (!this.nodeKey) {
      throw new Error('[Tree] nodeKey is required in setCurrentKey')
    }
    this.store.setCurrentNodeKey(key)
  }

  private getNode (data) {
    return this.store.getNode(data)
  }

  private remove (data) {
    this.store.remove(data)
  }

  private append (data, parentNode) {
    this.store.append(data, parentNode)
  }

  private insertBefore (data, refNode) {
    this.store.insertBefore(data, refNode)
  }

  private insertAfter (data, refNode) {
    this.store.insertAfter(data, refNode)
  }

  private handleNodeExpand (nodeData, node, instance) {
    this.dispatchEmitter('ElTreeNode', 'tree-node-expand', node)
    this.$emit('node-expand', nodeData, node, instance)
  }

  private updateKeyChildren (key, data) {
    if (!this.nodeKey) {
      throw new Error('[Tree] nodeKey is required in updateKeyChild')
    } 
    this.store.updateChildren(key, data)
  }

  private initTabIndex () {
    this.treeItems = this.$el.querySelectorAll('.is-focusable[role=treeitem]')
    this.checkboxItems = this.$el.querySelectorAll('input[type=checkbox]')
    const checkedItem: any = this.$el.querySelectorAll('.is-checked[role=treeitem]')
    if (checkedItem.length) {
      checkedItem[0].setAttribute('tabindex', 0)
      return
    }
    if (this.treeItems[0]) {
      this.treeItems[0].setAttribute('tabindex', 0)
    }
  }

  private handelKeydown (ev) {
    const currentItem = ev.target
    if (currentItem.className.indexOf('el-tree-node') === -1) {
      return
    }
    ev.preventDefault()
    const keyCode: any = ev.keyCode
    this.treeItems = this.$el.querySelectorAll('.is-focusable[role=treeitem]')
    const currentIndex = this.treeItemArray.indexOf(currentItem)
    let nextIndex
    if ([38, 40].indexOf(keyCode) > -1) { // up、down
      if (keyCode === 38) { // up
        nextIndex = currentIndex !== 0 ? currentIndex - 1 : 0
      } else {
        nextIndex = (currentIndex < this.treeItemArray.length - 1) ? currentIndex + 1 : 0
      }
      this.treeItemArray[nextIndex].focus() // 选中
    }
    if ([37, 39].indexOf(keyCode) > -1) { // left、right 展开
      currentItem.click() // 选中
    }
    const hasInput = currentItem.querySelector('[type="checkbox"]')
    if ([13, 32].indexOf(keyCode) > -1 && hasInput) { // space enter选中checkbox
      hasInput.click()
    }
  }

  private created () {
    (this as any).isTree = true
    this.store = new TreeStore({
      key: this.nodeKey,
      data: this.data,
      lazy: this.lazy,
      props: this.props,
      load: this.load,
      currentNodeKey: (this as any).currentNodeKey,
      checkStrictly: this.checkStrictly,
      checkDescendants: this.checkDescendants,
      defaultCheckedKeys: this.defaultCheckedKeys,
      defaultExpandedKeys: this.defaultExpandedKeys,
      autoExpandParent: this.autoExpandParent,
      defaultExpandAll: this.defaultExpandAll,
      filterNodeMethod: this.filterNodeMethod
    })
    this.root = this.store.root

    let dragState: any = this.dragState
    this.$on('tree-node-drag-start', (event, treeNode) => {
      if (typeof this.allowDrag === 'function' && !this.allowDrag(treeNode.node)) {
        event.preventDefault()
        return false
      }
      event.dataTransfer.effectAllowed = 'move'

      // wrap in try catch to address IE's error when first param is 'text/plain'
      try {
        // setData is required for draggable to work in FireFox
        // the content has to be '' so dragging a node out of the tree won't open a new tab in FireFox
        event.dataTransfer.setData('text/plain', '')
      } catch (e) {
        console.log(e)
      }
      dragState.draggingNode = treeNode
      this.$emit('node-drag-start', treeNode.node, event)
    })

    this.$on('tree-node-drag-over', (event, treeNode) => {
      const dropNode = findNearestComponent(event.target, 'ElTreeNode')
      const oldDropNode: any = dragState.dropNode
      if (oldDropNode && oldDropNode !== dropNode) {
        removeClass(oldDropNode.$el, 'is-drop-inner')
      }
      const draggingNode: any = dragState.draggingNode
      if (!draggingNode || !dropNode) {
        return
      }
      let dropPrev = true
      let dropInner = true
      let dropNext = true
      if (typeof this.allowDrop === 'function') {
        dropPrev = this.allowDrop(draggingNode.node, dropNode.node, 'prev')
        dropInner = this.allowDrop(draggingNode.node, dropNode.node, 'inner')
        dropNext = this.allowDrop(draggingNode.node, dropNode.node, 'next')
      }
      dragState.allowDrop = dropInner
      event.dataTransfer.dropEffect = dropInner ? 'move' : 'none'
      if ((dropPrev || dropInner || dropNext) && oldDropNode !== dropNode) {
        if (oldDropNode) {
          this.$emit('node-drag-leave', draggingNode.node, oldDropNode.node, event)
        }
        this.$emit('node-drag-enter', draggingNode.node, dropNode.node, event)
      }

      if (dropPrev || dropInner || dropNext) {
        dragState.dropNode = dropNode
      }

      if (dropNode.node.nextSibling === draggingNode.node) {
        dropNext = false
      }
      if (dropNode.node.previousSibling === draggingNode.node) {
        dropPrev = false
      }
      if (dropNode.node.contains(draggingNode.node, false)) {
        dropInner = false
      }
      if (draggingNode.node === dropNode.node || draggingNode.node.contains(dropNode.node)) {
        dropPrev = false
        dropInner = false
        dropNext = false
      }

      const targetPosition = dropNode.$el.querySelector('.el-tree-node__expand-icon').getBoundingClientRect()
      const treePosition = this.$el.getBoundingClientRect()

      let dropType
      const prevPercent = dropPrev ? (dropInner ? 0.25 : (dropNext ? 0.5 : 1)) : -1
      const nextPercent = dropNext ? (dropInner ? 0.75 : (dropPrev ? 0.5 : 0)) : 1

      let indicatorTop = -9999
      const distance = event.clientY - targetPosition.top
      if (distance < targetPosition.height * prevPercent) {
        dropType = 'before'
      } else if (distance > targetPosition.height * nextPercent) {
        dropType = 'after'
      } else if (dropInner) {
        dropType = 'inner'
      } else {
        dropType = 'none'
      }

      const dropIndicator: any = this.$refs.dropIndicator
      if (dropType === 'before') {
        indicatorTop = targetPosition.top - treePosition.top
      } else if (dropType === 'after') {
        indicatorTop = targetPosition.bottom - treePosition.top
      }
      dropIndicator.style.top = indicatorTop + 'px'
      dropIndicator.style.left = (targetPosition.right - treePosition.left) + 'px'

      if (dropType === 'inner') {
        addClass(dropNode.$el, 'is-drop-inner')
      } else {
        removeClass(dropNode.$el, 'is-drop-inner')
      }

      dragState.showDropIndicator = dropType === 'before' || dropType === 'after'
      dragState.dropType = dropType
      this.$emit('node-drag-over', draggingNode.node, dropNode.node, event)
    })

    this.$on('tree-node-drag-end', (event) => {
      const { draggingNode, dropType, dropNode } = dragState
      event.preventDefault()
      event.dataTransfer.dropEffect = 'move'

      if (draggingNode && dropNode) {
        const data = draggingNode.node.data
        if (dropType === 'before') {
          draggingNode.node.remove()
          dropNode.node.parent.insertBefore({ data }, dropNode.node)
        } else if (dropType === 'after') {
          draggingNode.node.remove()
          dropNode.node.parent.insertAfter({ data }, dropNode.node)
        } else if (dropType === 'inner') {
          dropNode.node.insertChild({ data })
          draggingNode.node.remove()
        }
        removeClass(dropNode.$el, 'is-drop-inner')

        this.$emit('node-drag-end', draggingNode.node, dropNode.node, dropType, event)
        if (dropType !== 'none') {
          this.$emit('node-drop', draggingNode.node, dropNode.node, dropType, event)
        }
      }
      if (draggingNode && !dropNode) {
        this.$emit('node-drag-end', draggingNode.node, null, dropType, event)
      }

      dragState.showDropIndicator = false
      dragState.draggingNode = null
      dragState.dropNode = null
      dragState.allowDrop = true
    })
  }

  private mounted () {
    this.$nextTick(() => {
      this.initTabIndex()
      this.$el.addEventListener('keydown', this.handelKeydown)
    })
  }

  private updated () {
    this.treeItems = this.$el.querySelectorAll('[role=treeitem]')
    this.checkboxItems = this.$el.querySelectorAll('input[type=checkbox]')
  }
}
</script>

